﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace MonoStrategy
{
    public delegate void DOnAddResourceStack<TSender>(TSender inSender, GenericResourceStack inStack);
    public delegate void DOnRemoveResourceStack<TSender>(TSender inSender, GenericResourceStack inStack);

    /// <summary>
    /// The resource manager, as its name implies, takes track of all resource stacks in the game.
    /// It also seeks for settlers carrying resources from provider to queries in a prioritized
    /// way. This is very important, since it ensures that queries with lower <see cref="VirtualCount"/>
    /// will come first, which has a significant positive impact on the whole economy. In the original
    /// game this was not enforced, this combined with the fact that there seemed to be a limit of
    /// resources being carried, it was just a burden if not impossible to get a huge settlement
    /// running properly.
    /// </summary>
    internal class ResourceManager : SynchronizedManager
    {
        /// <summary>
        /// All resources and queries are put into this list.
        /// </summary>
        private readonly TopologicalList<GenericResourceStack> m_Resources;
        private readonly TopologicalList<Foilage> m_Foilage;
        private readonly TopologicalList<Stone> m_Stones;
        private readonly LinkedList<GenericResourceStack> m_PriorityQueries = new LinkedList<GenericResourceStack>();

        /// <summary>
        /// All queries will also go here, as long as their <see cref="VirtualCount"/> is
        /// less than their <see cref="MaxStack"/> size. If this is the case you will find
        /// the query in the list with index <see cref="VirtualCount"/>.
        /// </summary>
        private readonly List<GenericResourceStack>[] m_Queries;
        /// <summary>
        /// The associated movable manager.
        /// </summary>
        internal MovableManager MovMgr { get; private set; }
        internal BuildingManager BuildMgr { get; set; }
        internal GameMap Map { get; private set; }
        internal TerrainDefinition Terrain { get { return MovMgr.Terrain; } }
        /// <summary>
        /// Is raised whenever a resource stack is added.
        /// </summary>
        internal event DOnAddResourceStack<ResourceManager> OnAddStack;
        /// <summary>
        /// Is raised whenever a resource stack is removed.
        /// </summary>
        internal event DOnRemoveResourceStack<ResourceManager> OnRemoveStack;
        internal event DOnAddFoilage<ResourceManager> OnAddFoilage;
        internal event DOnRemoveFoilage<ResourceManager> OnRemoveFoilage;
        internal event DOnAddStone<ResourceManager> OnAddStone;
        internal event DOnRemoveStone<ResourceManager> OnRemoveStone;

        internal ResourceManager(GameMap inMap, MovableManager inMovMgr) : base(inMovMgr)
        {
            if ((inMovMgr == null) || (inMap == null))
                throw new ArgumentNullException();

            Map = inMap;
            MovMgr = inMovMgr;
            m_Resources = new TopologicalList<GenericResourceStack>(10, inMovMgr.Size, inMovMgr.Size);
            m_Foilage = new TopologicalList<Foilage>(10, inMovMgr.Size, inMovMgr.Size);
            m_Stones = new TopologicalList<Stone>(10, inMovMgr.Size, inMovMgr.Size);
            m_Queries = new List<GenericResourceStack>[GenericResourceStack.DEFAULT_STACK_SIZE];

            for (int i = 0; i < m_Queries.Length; i++)
            {
                m_Queries[i] = new List<GenericResourceStack>();
            }
        }

        void inMovMgr_OnCellChanged(MovableManager sender, Point cell)
        {
            var pos = new Point(cell.X, cell.Y);

            if (MovMgr.IsWalkable(pos, 0))
                return;

            var stacks = m_Resources.EnumAt(pos);

            foreach (var stack in stacks)
            {
                if (stack.Type == ResStackType.Query)
                    throw new InvalidOperationException("Attempt to make a cell, containing a resource query, unwalkable.");

                RemoveResource(stack);
            }
        }

        /// <summary>
        /// Adds a new resource stack with the given parameters and raises <see cref="OnAddStack"/>.
        /// This method is also called internally and it is crucial to do all postprocessings meant to
        /// be applied for all resource stacks, like attaching a visual for rendering, in the mentioned event.
        /// </summary>
        /// <exception cref="ArgumentException">There is already a resource or the given position is not walkable.</exception>
        internal GenericResourceStack AddResourceStack(CyclePoint inPosition, ResStackType inType, Resource inResource, Int32 inMaxStack)
        {
            return AddResourceStack(null, inPosition, inType, inResource, inMaxStack);
        }

        internal bool CanPlaceResourceAt(Point inPosition)
        {
            for (int x = -1; x <= 1; x++)
            {
                for (int y = -1; y <= 1; y++)
                {
                    var pos = new Point(inPosition.X + x, inPosition.Y + y);

                    if ((pos.X < 0) || (pos.Y < 0) || (pos.X >= Terrain.Size) || (pos.Y >= Terrain.Size))
                        return false;

                    if (m_Resources.EnumAt(pos).Count() > 0)
                        return false;
                }
            }

            if (MovMgr.Terrain.GetWallAt(inPosition.X, inPosition.Y) > WallValue.Reserved)
                return false;

            return true;
        }

        internal GenericResourceStack AddResourceStack(BaseBuilding inParent, CyclePoint inPosition, ResStackType inType, Resource inResource, Int32 inMaxStack)
        {
            //if (m_Resources.EnumAt(inPosition.ToPoint()).Count() > 0)
            //    throw new ArgumentException("There is already a resource at the given position.");

            //if (MovMgr.Terrain.GetWallAt(inPosition.XGrid, inPosition.YGrid) > WallValue.Reserved)
                //throw new ArgumentException("Given position is locked or unwalkable.");

            GenericResourceStack stack = new GenericResourceStack(inParent, inType, inResource, (inMaxStack > 0) ? inMaxStack : GenericResourceStack.DEFAULT_STACK_SIZE);

            stack.Position = inPosition;

            if (stack.Type == ResStackType.Query)
            {
                stack.OnCountChanged += OnStackCountChanged;
                stack.OnMaxCountChanged += OnMaxStackCountChanged;

                m_Queries[0].Add(stack);
            }

            MovMgr.Terrain.InitializeWallAt(new Point(inPosition.XGrid, inPosition.YGrid), WallValue.Reserved, new Rectangle(0,0,1,1));

            m_Resources.Add(stack);

            if (OnAddStack != null)
                OnAddStack(this, stack);

            return stack;
        }

        private void OnMaxStackCountChanged(GenericResourceStack inSender, int inOldMaxCount, int inNewMaxCount)
        {
            if (inSender.IsRemoved)
                return;

            if (inOldMaxCount > inNewMaxCount)
            {
                // max count is being decreased
                if (inSender.VirtualCount >= inNewMaxCount)
                {
                    // remove from queue
                    if (!m_Queries[inSender.VirtualCount].Remove(inSender))
                        throw new ApplicationException("Resource query is not registered in queue.");
                }
            }
            else
            {
                // max count is being increased
                if ((inSender.VirtualCount >= inOldMaxCount) && (inSender.VirtualCount < inNewMaxCount))
                {
                    // add to new queue
                    m_Queries[inSender.VirtualCount].Add(inSender);
                }
            }
        }

        /// <summary>
        /// For resource queries we need to keep track of their stack size, since we have
        /// to relocate them appropriately in the priority queue.
        /// </summary>
        private void OnStackCountChanged(GenericResourceStack inSender, int inOldValue, int inNewValue)
        {
            if (inSender.IsRemoved)
                return;

            if (inOldValue < inSender.MaxCount)
            {
                // remove from old queue
                if (!m_Queries[inOldValue].Remove(inSender))
                    throw new ApplicationException("Resource query is not registered in queue.");
            }

            if (inNewValue < inSender.MaxCount)
            {
                // add to new queue
                m_Queries[inNewValue].Add(inSender);
            }
        }

        /// <summary>
        /// If no resource exists at the given grid point, a new resource provider is added and
        /// filled. Otherwise the existing stack is used if it is of the same resource type.
        /// If <paramref name="inCount"/> exceeds the remaining stack capacity, the method enumerates
        /// around the given point and repeats the task until all desired resources are placed.
        /// </summary>
        /// <remarks>
        /// This is an important method also demonstrating how powerful our lightweight resource
        /// concept is. With almost no effort we can simulate resource drops in the same way they
        /// are done in the original game.
        /// </remarks>
        /// <param name="inAround">You may also pass unwalkable spots. The method enumerates until it finds one.</param>
        /// <param name="inResource">The resource type being dropped.</param>
        /// <param name="inCount">Amount of resources being dropped.</param>
        internal void DropResource(Point inAround, Resource inResource, int inCount)
        {
            DropResourceInternal(inAround, inResource, inCount, null);
        }

        internal void DropMarketResource(MarketBuilding inMarket, Point inAround, Resource inResource, int inCount)
        {
            DropResourceInternal(inAround, inResource, inCount, inMarket.UniqueID);
        }

        private void DropResourceInternal(Point inAround, Resource inResource, int inCount, Int64? inMinProvID)
        {
            Terrain.EnumAround(inAround, (pos) =>
            {
                IEnumerable<GenericResourceStack> stackEnum = m_Resources.EnumAt(pos);
                GenericResourceStack stack;

                if (stackEnum.Count() > 0)
                {
                    // merge with existing stacks
                    stack = stackEnum.First();

                    if ((stack.Type == ResStackType.Query) || (stack.Resource != inResource))
                        return WalkResult.NotFound;
                }
                else
                {
                    // add a new resource provider
                    if (!CanPlaceResourceAt(pos))
                        return WalkResult.NotFound;

                    stack = AddResourceStack(CyclePoint.FromGrid(pos), ResStackType.Provider, inResource, 0);
                }

                int freeCount = stack.MaxCount - stack.VirtualCount;

                if (freeCount <= 0)
                    return WalkResult.NotFound;

                freeCount = Math.Min(freeCount, inCount);

                for (int i = 0; i < freeCount; i++)
                {
                    stack.AddResource();
                }

                if (inMinProvID != null)
                    stack.MinProviderID = inMinProvID.Value;

                inCount -= freeCount;

                if (inCount <= 0)
                    return WalkResult.Success;
                else
                    return WalkResult.NotFound;
            });
        }

        internal GenericResourceStack FindResourceAround(Point inAround, Resource inResource, ResStackType inStackType)
        {
            return FindResourceAroundInternal(inAround, inResource, inStackType, 0);
        }

        internal GenericResourceStack FindResourceAroundInternal(Point inAround, Resource inResource, ResStackType inStackType, Int64 inQueryID)
        {
            GenericResourceStack result = null;

            m_Resources.EnumAround(inAround, (stack) =>
            {
                if (stack.Type != inStackType)
                    return WalkResult.NotFound;

                if (stack.Resource != inResource)
                    return WalkResult.NotFound;

                if (stack.VirtualCount <= 0)
                    return WalkResult.NotFound;

                if (inQueryID > 0)
                {
                    // To prevent resources from being carried back to the origin (market)
                    if (stack.MinProviderID >= inQueryID)
                        return WalkResult.NotFound;
                }

                result = stack;

                return WalkResult.Success;
            });

            return result;
        }


        /// <summary>
        /// Removes the given stack. Please keep in mind that this method is also called
        /// internally and it is crucial that you put ALL additional code required to release
        /// the resource stack into the event handler <see cref="OnRemoveStack"/>, like
        /// detaching it from the render pipeline.
        /// </summary>
        /// <exception cref="ArgumentNullException">No resource given.</exception>
        /// <exception cref="ApplicationException">Resource does not exist.</exception>
        internal void RemoveResource(GenericResourceStack inResource)
        {
            if (inResource == null)
                throw new ArgumentNullException();

            m_Resources.Remove(inResource);

            if (inResource.PriorityNode != null)
                m_PriorityQueries.Remove(inResource.PriorityNode);

            inResource.PriorityNode = null;

            // remove from query queue
            if (inResource.Type == ResStackType.Query)
            {
                if (inResource.VirtualCount < inResource.MaxCount)
                {
                    if (!m_Queries[inResource.VirtualCount].Remove(inResource))
                        throw new ApplicationException("Resource query is not registered in queue.");
                }
            }

            inResource.MarkAsRemoved();

            if (OnRemoveStack != null)
                OnRemoveStack(this, inResource);
        }

        internal void RaisePriority(GenericResourceStack inStack)
        {
            if (inStack.Type != ResStackType.Query)
                throw new InvalidOperationException();

            LowerPriority(inStack);

            inStack.PriorityNode = m_PriorityQueries.AddFirst(inStack);
        }

        internal void LowerPriority(GenericResourceStack inStack)
        {
            if (!inStack.HasPriority)
                return;

            m_PriorityQueries.Remove(inStack.PriorityNode);
            inStack.PriorityNode = null;
        }

        /// <summary>
        /// Will do all the job planning.
        /// </summary>
        internal override void ProcessCycle()
        {
            base.ProcessCycle();

            if (!IsAlignedCycle)
                return;

            // process high priorities
            foreach (var query in m_PriorityQueries)
            {
                for (int i = 0, count = query.MaxCount - query.VirtualCount; i < count; i++ )
                {
                    ProcessQuery(query);
                }
            }

            // process BuildTask resources
            if (BuildMgr != null)
            {
                BuildMgr.ProcessQueries((query) =>
                {
                    if (query.VirtualCount < query.MaxCount)
                        ProcessQuery(query);
                });
            }

            // process normal priorities
            foreach (var queue in m_Queries)
            {
                foreach (var query in queue.ToArray())
                {
                    ProcessQuery(query);
                }
            }
        }

        private void ProcessQuery(GenericResourceStack query)
        {
            Debug.Assert(query.Type == ResStackType.Query);

            // special handling for MineBuilding queries
            if ((query.Building != null) && (query.Building is MineBuilding))
            {
                var dist = Map.Config.StockDistributions;
                bool isQueryEnabled = false;

                for (int i = 0; i < dist.Length; i++)
                {
                    if (dist[i].Building == query.Building.Config)
                    {
                        isQueryEnabled = dist[i].Queries[query.Resource];

                        break;
                    }
                }

                if (!isQueryEnabled)
                    return;
            }

            // special handling for MarketBuilding queries
            Int64 queryID = 0;

            if ((query.Building != null) && (query.Building is MarketBuilding))
            {
                var market = (query.Building as MarketBuilding);

                queryID = market.UniqueID;
            }

            // search for matching provider
            GenericResourceStack provider = FindResourceAroundInternal(query.Position.ToPoint(), query.Resource, ResStackType.Provider, queryID);

            if (provider == null)
                return;

            // find settler around provider
            Movable movable = MovMgr.FindFreeMovableAround(provider.Position.ToPoint(), MovableType.Settler);

            if (movable == null)
                return;

            // carry resource...
            JobCarrying job = new JobCarrying(movable, provider, query);
            movable.Job = job;

            job.OnPickedUp += (unused) => { job.AnimationClass = "Carrying_" + movable.Carrying.Value; };
            job.OnCompleted += (unused, succeeded) => { movable.Job = null; };
            job.Update();
        }

        internal void CountResources(int[] inBuffer)
        {
            Array.Clear(inBuffer, 0, inBuffer.Length);

            m_Resources.ForEach((stack) =>
            {
                inBuffer[(int)stack.Resource] += stack.VirtualCount;

                return true;
            });
        }

        internal void RemoveFoilage(Foilage inFoilage)
        {
            m_Foilage.Remove(inFoilage);

            Terrain.SetWallAt(inFoilage.Position.ToPoint(), WallValue.Free, new Rectangle(-1, -1, 3, 3));

            if (OnRemoveFoilage != null)
                OnRemoveFoilage(this, inFoilage);
        }

        internal void AddFoilage(Point inPosition, FoilageType inType, FoilageState inState)
        {
            Point around = inPosition;

            var checkResult = GridSearch.GridWalkAround(around, Terrain.Size, Terrain.Size, (pos) =>
            {
                if (Math.Abs(pos.X - around.X) > 5)
                    return WalkResult.Abort;

                if (!Terrain.CanFoilageBePlacedAt(pos.X, pos.Y, inType))
                    return WalkResult.NotFound;

                inPosition = pos;

                return WalkResult.Success;
            });

            if (checkResult != WalkResult.Success)
                return; // it's not considred essential enough to throw an exception when foilage can not be placed around target...

            // reserve map space
            Terrain.SetWallAt(inPosition, WallValue.Building);
            //Terrain.SetFlagsAt(inPosition.X, inPosition.Y, TerrainCellFlags.Grading);
            Terrain.InitializeWallAt(inPosition, WallValue.Reserved, new Rectangle(-1, -1, 3, 3));

            Foilage foilage = new Foilage(inPosition, inType, inState);

            m_Foilage.Add(foilage);

            if (inState == FoilageState.Growing)
            {
                QueueWorkItem(VisualUtilities.GetDurationMillis(foilage, "Growing"), () =>
                {
                    foilage.MarkAsGrown();
                });
            }

            if (OnAddFoilage != null)
                OnAddFoilage(this, foilage);
        }

        internal Foilage FindFoilageAround(Point inAround, int inRadius, FoilageType inType, FoilageState inState)
        {
            Foilage result = null;

            if (WalkResult.Success != m_Foilage.EnumAround(inAround, inRadius, (foilage) =>
                    {
                        if ((foilage.State != inState) || ((foilage.Type & inType) == 0))
                            return WalkResult.NotFound;

                        result = foilage;

                        return WalkResult.Success;
                    }))
                return null;

            return result;
        }

        internal int CountFoilageAround(Point inAround, int inRadius, FoilageType inType, FoilageState inState)
        {
            int count = 0;

            m_Foilage.EnumAround(inAround, inRadius, (foilage) =>
            {
                if ((foilage.State != inState) || ((foilage.Type & inType) == 0))
                    return WalkResult.NotFound;

                count++;

                return WalkResult.NotFound;
            });

            return count;
        }

        internal WalkResult EnumFoilageAround(Point inAround, int inRadius, Func<Foilage, WalkResult> inHandler)
        {
            return m_Foilage.EnumAround(inAround, inRadius, (foilage) =>
            {
                return inHandler(foilage);
            });
        }

        internal void RemoveStone(Stone inStone)
        {
            m_Stones.Remove(inStone);

            Terrain.SetWallAt(inStone.Position.ToPoint(), WallValue.Free, new Rectangle(-1, -1, 3, 3));

            if (OnRemoveStone != null)
                OnRemoveStone(this, inStone);
        }

        internal void AddStone(Point inPosition, int inInitialStoneCount)
        {
            Point around = inPosition;

            var checkResult = GridSearch.GridWalkAround(around, Terrain.Size, Terrain.Size, (pos) =>
            {
                if (Math.Abs(pos.X - around.X) > 5)
                    return WalkResult.Abort;

                if (!Terrain.CanFoilageBePlacedAt(pos.X, pos.Y, FoilageType.Tree1))
                    return WalkResult.NotFound;

                inPosition = pos;

                return WalkResult.Success;
            });

            if (checkResult != WalkResult.Success)
                return; // it's not considred essential enough to throw an exception when stone can not be placed around target...

            // reserve map space
            Terrain.SetWallAt(inPosition, WallValue.Building);
            Terrain.InitializeWallAt(inPosition, WallValue.Reserved, new Rectangle(-1, -1, 3, 3));

            Stone stone = new Stone(inPosition, inInitialStoneCount);

            m_Stones.Add(stone);

            if (OnAddStone != null)
                OnAddStone(this, stone);
        }

        internal Stone FindStoneAround(Point inAround, int inRadius)
        {
            Stone result = null;

            if (WalkResult.Success != m_Stones.EnumAround(inAround, inRadius, (stone) =>
            {
                if (stone.RemainingStones <= 0)
                    return WalkResult.NotFound;

                result = stone;

                return WalkResult.Success;
            }))
                return null;

            return result;
        }
    }
}
